// Copyright 2022 Jeroen van Nugteren

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// include header
#include "dfrrectangle.hh"

// code specific to Rat
namespace rat{namespace dm{

	// constructor
	DFRRectangle::DFRRectangle(){
		set_name("Rounded Rectangle");
	}

	// constructcor
	DFRRectangle::DFRRectangle(
		const fltp x1, const fltp x2, 
		const fltp y1, const fltp y2, 
		const fltp radius) : DFRRectangle(){
		// set to self
		set_x1(x1); set_x2(x2);
		set_y1(y1); set_y2(y2);
		set_radius(radius);

		// check validity
		is_valid(true);
	}

	// factory
	ShDFRRectanglePr DFRRectangle::create(){
		return std::make_shared<DFRRectangle>();
	}

	// factory
	ShDFRRectanglePr DFRRectangle::create(
		const fltp x1, const fltp x2, 
		const fltp y1, const fltp y2, 
		const fltp radius){
		return std::make_shared<DFRRectangle>(x1,x2,y1,y2,radius);
	}

	// setters
	void DFRRectangle::set_x1(const fltp x1){
		x1_ = x1;
	}

	void DFRRectangle::set_x2(const fltp x2){
		x2_ = x2;
	}

	void DFRRectangle::set_y1(const fltp y1){
		y1_ = y1;
	}

	void DFRRectangle::set_y2(const fltp y2){
		y2_ = y2;
	}

	void DFRRectangle::set_radius(const fltp radius){
		radius_ = radius;
	}

	// getters
	fltp DFRRectangle::get_x1()const{
		return x1_;
	}

	fltp DFRRectangle::get_x2()const{
		return x2_;
	}

	fltp DFRRectangle::get_y1()const{
		return y1_;
	}

	fltp DFRRectangle::get_y2()const{
		return y2_;
	}

	fltp DFRRectangle::get_radius()const{
		return radius_;
	}

	// perimeter function
	ShPerimeterPr DFRRectangle::create_perimeter(const fltp delem) const{
		// get element size
		const arma::uword nelem1 = arma::uword(std::ceil((x2_-x1_-2*radius_)/delem));
		const arma::uword nelem2 = arma::uword(std::ceil((y2_-y1_-2*radius_)/delem));

		// arc
		const arma::uword nelemarc = arma::uword(std::ceil(0.5*arma::Datum<fltp>::pi*radius_/delem));

		// side1 
		const arma::Row<fltp> x1 = arma::linspace<arma::Row<fltp> >(x1_+radius_,x2_-radius_,nelem1+1);
		const arma::Row<fltp> y1 = arma::linspace<arma::Row<fltp> >(y1_,y1_,nelem1+1);

		// arc1	
		const arma::Row<fltp> theta1 = arma::linspace<arma::Row<fltp> >(-arma::Datum<fltp>::pi/2,0,nelemarc+1);
		const arma::Row<fltp> rho1 = arma::linspace<arma::Row<fltp> >(radius_,radius_,nelemarc+1);
		const arma::Row<fltp> x2 = rho1%arma::cos(theta1) + x2_ - radius_;
		const arma::Row<fltp> y2 = rho1%arma::sin(theta1) + y1_ + radius_;

		// side2 
		const arma::Row<fltp> x3 = arma::linspace<arma::Row<fltp> >(x2_,x2_,nelem2+1);
		const arma::Row<fltp> y3 = arma::linspace<arma::Row<fltp> >(y1_+radius_,y2_-radius_,nelem2+1);

		// arc2	
		const arma::Row<fltp> theta2 = arma::linspace<arma::Row<fltp> >(0,arma::Datum<fltp>::pi/2,nelemarc+1);
		const arma::Row<fltp> rho2 = arma::linspace<arma::Row<fltp> >(radius_,radius_,nelemarc+1);
		const arma::Row<fltp> x4 = rho2%arma::cos(theta2) + x2_ - radius_;
		const arma::Row<fltp> y4 = rho2%arma::sin(theta2) + y2_ - radius_;

		// side3 
		const arma::Row<fltp> x5 = arma::linspace<arma::Row<fltp> >(x2_-radius_,x1_+radius_,nelem1+1);
		const arma::Row<fltp> y5 = arma::linspace<arma::Row<fltp> >(y2_,y2_,nelem1+1);

		// arc3	
		const arma::Row<fltp> theta3 = arma::linspace<arma::Row<fltp> >(arma::Datum<fltp>::pi/2,arma::Datum<fltp>::pi,nelemarc+1);
		const arma::Row<fltp> rho3 = arma::linspace<arma::Row<fltp> >(radius_,radius_,nelemarc+1);
		const arma::Row<fltp> x6 = rho3%arma::cos(theta3) + x1_ + radius_;
		const arma::Row<fltp> y6 = rho3%arma::sin(theta3) + y2_ - radius_;

		// side4 
		const arma::Row<fltp> x7 = arma::linspace<arma::Row<fltp> >(x1_,x1_,nelem2+1);
		const arma::Row<fltp> y7 = arma::linspace<arma::Row<fltp> >(y2_-radius_,y1_+radius_,nelem2+1);

		// arc4	
		const arma::Row<fltp> theta4 = arma::linspace<arma::Row<fltp> >(arma::Datum<fltp>::pi,(3.0/2)*arma::Datum<fltp>::pi,nelemarc+1);
		const arma::Row<fltp> rho4 = arma::linspace<arma::Row<fltp> >(radius_,radius_,nelemarc+1);
		const arma::Row<fltp> x8 = rho4%arma::cos(theta4) + x1_ + radius_;
		const arma::Row<fltp> y8 = rho4%arma::sin(theta4) + y1_ + radius_;

		// combine
		const arma::Mat<fltp> Rn = arma::join_vert(
			arma::join_horiz(arma::join_horiz(x1.cols(0,nelem1-1),x2.cols(0,nelemarc-1),x3.cols(0,nelem2-1),x4.cols(0,nelemarc-1)),arma::join_horiz(x5.cols(0,nelem1-1),x6.cols(0,nelemarc-1),x7.cols(0,nelem2-1),x8.cols(0,nelemarc-1))),
			arma::join_horiz(arma::join_horiz(y1.cols(0,nelem1-1),y2.cols(0,nelemarc-1),y3.cols(0,nelem2-1),y4.cols(0,nelemarc-1)),arma::join_horiz(y5.cols(0,nelem1-1),y6.cols(0,nelemarc-1),y7.cols(0,nelem2-1),y8.cols(0,nelemarc-1))));

		// create element matrix
		const arma::Mat<arma::uword> n=arma::join_vert(
			arma::regspace<arma::Row<arma::uword> >(0,Rn.n_cols-1),
			arma::shift(arma::regspace<arma::Row<arma::uword> >(0,Rn.n_cols-1),-1,1));

	 	// create perimeter
		return Perimeter::create(Rn,n);
	}

	// get bounding box
	arma::Mat<fltp>::fixed<2,2> DFRRectangle::get_bounding() const{
		// allocate
		arma::Mat<fltp>::fixed<2,2> Mb;
		
		// bounding in x
		Mb.col(0) = arma::Col<fltp>::fixed<2>{x1_,x2_};
		
		// bounding in y
		Mb.col(1) = arma::Col<fltp>::fixed<2>{y1_,y2_};

		// return box
		return Mb;
	}

	// get fixed points
	arma::Mat<fltp> DFRRectangle::get_fixed(const fltp /*abstol*/) const{
		// create list of fixed points
		arma::Mat<fltp> pfix(8,2);
		pfix.row(0) = arma::Row<fltp>{x1_+radius_,y1_};
		pfix.row(1) = arma::Row<fltp>{x1_,y1_+radius_};
		pfix.row(2) = arma::Row<fltp>{x1_+radius_,y2_};
		pfix.row(3) = arma::Row<fltp>{x1_,y2_-radius_};
		pfix.row(4) = arma::Row<fltp>{x2_-radius_,y1_};
		pfix.row(5) = arma::Row<fltp>{x2_,y1_+radius_};
		pfix.row(6) = arma::Row<fltp>{x2_-radius_,y2_};
		pfix.row(7) = arma::Row<fltp>{x2_,y2_-radius_};
		return pfix;
	}

	// distance function
	arma::Col<fltp> DFRRectangle::calc_distance(const arma::Mat<fltp> &p) const{
		// create union
		const ShDFUnionPr df = DFUnion::create();

		// add rectangles
		df->add(DFRectangle::create(x1_,x2_,y1_+radius_,y2_-radius_));
		df->add(DFRectangle::create(x1_+radius_,x2_-radius_,y1_,y2_));

		// add circles for corners
		df->add(DFCircle::create(radius_,x1_+radius_,y1_+radius_));
		df->add(DFCircle::create(radius_,x2_-radius_,y1_+radius_));
		df->add(DFCircle::create(radius_,x1_+radius_,y2_-radius_));
		df->add(DFCircle::create(radius_,x2_-radius_,y2_-radius_));

		// return distance
		return df->calc_distance(p);
	}

	// validity check
	bool DFRRectangle::is_valid(const bool enable_throws)const{
		// check user input
		if(x2_-x1_<2*radius_){if(enable_throws){rat_throw_line("x2 minux x1 must be larger than 2 radii");} return false;};
		if(y2_-y1_<2*radius_){if(enable_throws){rat_throw_line("y2 minux y1 must be larger than 2 radii");} return false;};
		if(radius_<=0){if(enable_throws){rat_throw_line("radius must be larger than zero");} return false;};
		return true;
	}

	// type string for serialization
	std::string DFRRectangle::get_type(){
		return "rat::dm::dfrrectangle";
	}

	// method for serialization into json
	void DFRRectangle::serialize(Json::Value &js, cmn::SList &list) const{
		// parent
		DistFun::serialize(js,list);

		// store type ID
		js["type"] = get_type();
		
		// properties
		js["x1"] = x1_; 
		js["x2"] = x2_;
		js["y1"] = y1_;	
		js["y2"] = y2_;
		js["radius"] = radius_;
	}

	// method for deserialisation from json
	void DFRRectangle::deserialize(
		const Json::Value &js, cmn::DSList &list, 
		const cmn::NodeFactoryMap &factory_list, 
		const boost::filesystem::path &pth){
		
		// parent
		DistFun::deserialize(js,list,factory_list,pth);

		// get properties
		set_x1(js["x1"].ASFLTP()); 
		set_x2(js["x2"].ASFLTP());
		set_y1(js["y1"].ASFLTP()); 
		set_y2(js["y2"].ASFLTP());
		set_radius(js["radius"].ASFLTP());
	}

}}